Skip to content

S00-00 General-Git

[TOC]

概述

什么是 Git

Git

  • 简单来说Git 是一个“代码的时间机器”和“多人协作的神器”。

  • 在技术层面上,Git 是目前世界上最先进的分布式版本控制系统(Distributed Version Control System),由 Linux 之父 Linus Torvalds 于 2005 年创建。

为了让你更直观地理解,我们可以从生活中的痛点游戏两个角度来看:

为什么需要 Git

场景一:后悔药(版本回退)

你写论文或做设计时,是不是经常会有这种文件命名:

  • 论文_最终版.doc
  • 论文_最终版_再改一下.doc
  • 论文_打死不改版.doc
  • 论文_真正的最终版.doc

Git 的作用: 它可以帮你记录文件的每一次改动(谁在什么时间改了哪一行)。你不需要存无数个备份文件,只需要一个文件。如果改错了,你可以随时“穿越”回昨天、上周甚至去年的任何一个状态。

场景二:游戏存档(Save Point)

玩 RPG 游戏打 Boss 之前,你通常会存一个档。如果打输了,就读取存档重来。

Git 的作用: 每当你完成一个功能,就提交(Commit) 一次,相当于存了一个档。如果后续代码写崩了,直接回档,安全无痛。

场景三:平行宇宙(分支管理)

想象你正在写一个软件的主线故事(正式版),突然想尝试一个疯狂的新功能(实验版)。

Git 的作用: 你可以开启一个“分支”(Branch),在这个平行宇宙里随意折腾,完全不会影响主线。如果实验成功,就把平行宇宙合并(Merge)回主线;如果失败,直接删掉这个宇宙,主线毫发无损。

核心工作流@

Git 的核心工作流

Git 的操作主要在三个区域之间流转,理解了这个图就理解了 Git 的一半:

  • 工作区 (Working Directory): 你电脑里能看到的文件夹,你平时就在这里修改代码。
  • 暂存区 (Staging Area / Index): 一个中间区域。就像购物时的“购物车”,你把想提交的文件先放进来 (git add),确认无误后再打包结算。
  • 本地仓库 (Local Repository): 安全的数据库。当你执行“提交” (git commit) 后,文件就被永久记录在这里了。

Git vs GitHub

Git vs GitHub

很多初学者容易搞混这两个概念,它们的关系就像 MP3 和 Spotify,或者 视频文件 和 YouTube

特性GitGitHub (或 GitLab/Gitee)
本质一个工具软件一个网站/云服务平台
安装位置安装在你的电脑上访问网页使用
主要功能本地管理版本、记录历史托管你的 Git 仓库、方便多人协作、代码备份
是否联网不需要,断网也能用需要联网

一句话总结: 你在本地用 Git 写代码、存版本,然后推送到 GitHub 上给别人看或备份。

优缺点

Git 是目前最主流的版本控制工具,占据了统治地位,但它并不完美。它的设计哲学非常极客(Geek),这也导致了它鲜明的优缺点。

以下是对 Git 优缺点的深度分析:

优点

Git 的核心优点

1. 真正“分布式”带来的安全与便捷

  • 每个人都拥有完整的历史记录: 当你从服务器下载(Clone)项目时,你不仅仅是下载了最新的文件,而是把整个项目的历史(每一次修改、每一个版本)都完整地复制到了你的电脑上。
  • 安全: 即使服务器硬盘爆炸了,任何一个成员电脑里的仓库都可以直接还原出服务器的数据。

2. 极其强大的分支(Branch)管理

  • 这是 Git 的“杀手锏”: 在 SVN 等老一代工具中,创建分支就像复制整个文件夹,速度慢且占空间。而在 Git 中,创建分支只是创建一个“指针”,瞬间完成。
  • 鼓励实验 因为开分支成本极低,Git 鼓励你为每一个小功能、每一个 Bug 修复都开一个新分支,互不干扰。

3. 速度极快

  • 因为大部分操作都在本地磁盘进行,不需要频繁连接服务器,所以 Git 的提交、差异比较(Diff)等操作速度极快。

4. 暂存区(Staging Area)设计

  • 这是 Git 独有的设计。它允许你把一部分修改先“放进购物车”(暂存),而不是必须一次性提交所有修改。这让你的提交记录(Commit Log)可以非常清晰、原子化。

5. 完整的数据保证

  • Git 使用 SHA-1 哈希算法来标识每一次提交。这意味着如果不改变 ID,就不可能在不知情的情况下修改文件内容或历史记录。你的代码历史是防篡改的。

缺点

Git 的主要缺点

1. 学习曲线陡峭(难上手)

  • 概念抽象: 对初学者来说,理解“工作区、暂存区、本地库、远程库”的数据流向非常头大。
  • 命令繁多且晦涩: Git 有上百个命令,而且参数复杂。出了错(比如 detached HEAD 状态),报错信息往往很“技术化”,新手很难看懂怎么修。
  • 容易“搞崩”心态: 很多新手都有过“我不小心合并错了,然后把代码搞丢了”的恐怖经历(其实很难真丢,但在新手看来就是丢了)。

2. 对大文件(二进制文件)支持不佳

  • Git 擅长管理文本文件(代码)。
  • 如果你把大量的图片、视频、编译后的 .exe.dll 文件塞进 Git,仓库体积会迅速膨胀。因为 Git 每次保存都会存一份副本,且这些二进制文件无法像文本那样进行“增量压缩”。
  • :虽然有Git LFS(Large File Storage)插件可以缓解,但原生体验依然不好。

3. 权限控制较弱

  • Git 的设计理念是“全有或全无”。通常情况下,你要么能访问整个仓库,要么都不能访问。
  • 很难像 SVN 那样精细地控制:让张三只能改 A 文件夹,李四只能改 B 文件夹。(这通常需要借助 GitHub/GitLab 等平台的额外功能来实现,而非 Git 本身的功能)。

4. 历史记录不可逆的“脏乱”

  • 虽然这算是一个双刃剑,但 Git 允许你修改历史(Rebase)。如果不小心强制推送(Force Push)覆盖了别人的代码,可能会导致团队协作的灾难。

基本使用

常用命令

为了让你更清晰地掌握 Git,我将这些命令按照实际开发的工作流进行分类。我不只列出命令,还会重点解释那些高频使用的参数,帮你避坑。

为了辅助理解,这张图展示了文件在不同区域流转时对应的命令:

配置与初始化

配置与初始化

在你开始写代码之前,必须先“报家门”。

  • git config设置用户信息

    • git config --global user.name "你的名字"
    • git config --global user.email "你的邮箱"
    • --global关键参数。表示这台电脑上所有的 Git 仓库都用这个配置。如果不加,就只对当前仓库有效。
    • git config --list:查看当前所有的配置信息。
  • git init把当前文件夹变成 Git 仓库

    • 执行后会生成一个隐藏的 .git 文件夹(千万别手动删它,否则历史记录全没)。
  • git clone [url]从远程(如 GitHub)下载一个仓库

    • git clone -b [分支名] [url]常用。直接克隆指定的某个分支,而不是默认的 master/main。

日常高频操作

日常高频操作

这是你每天哪怕闭着眼都要敲几十遍的命令。

  • git status查看当前仓库状态(有哪些改动?在哪个区?)

    • git status -s常用。以精简模式显示(Short),看着不累。
  • git add把文件从“工作区”放入“暂存区”

    • git add [文件名]:添加指定文件。
    • git add .最常用。添加当前目录下的所有变动(新建的、修改的),但不包括被删除的文件
    • git add -A:添加所有变动(包括删除的)。
  • git commit把“暂存区”的内容提交到“本地仓库”(存档)

    • git commit -m "提交信息"必须带-m 后面写清楚你干了什么。
    • git commit -a -m "信息"偷懒神器。跳过 git add 步骤,直接把所有已跟踪文件的修改提交(注意:对新建的未跟踪文件无效)。
    • git commit --amend后悔药。如果你刚才提交完发现漏了个文件,或者注释写错了,用这个命令可以修改“上一次提交”,而不会产生新的提交记录。

分支管理

分支管理

Git 的灵魂。

  • git branch查看或操作分支

    • git branch:列出本地所有分支,当前分支前会有 *
    • git branch [新分支名]:创建一个新分支,但不切换过去。
    • git branch -d [分支名]:删除已合并的分支。
    • git branch -D [分支名]强力删除。如果分支没合并过,用小写 -d 删不掉,必须用大写 -D
  • git switch / git checkout切换分支

    • git switch [分支名]推荐(新版本命令)。纯粹用于切换分支。
    • git switch -c [新分支名]常用创建并立即切换到新分支(等同于 git checkout -b)。
    • git checkout [分支名]:老版本命令,功能太杂(既能切分支又能恢复文件),建议慢慢改用 switch。
  • git merge合并分支

    • git merge [分支名]:把指定分支合并到当前分支
    • git merge --no-ff [分支名]建议使用。关闭“快进模式”(No Fast Forward)。这样合并会在历史记录里生成一个新的节点,能清晰看出这里发生过一次合并。

远程同步

远程同步

与同事协作、与 GitHub 服务器交互。

  • git pull拉取远程代码并合并(= fetch + merge)

    • git pull origin main:拉取远程 main 分支并合并。
    • git pull --rebase高手习惯。拉取代码时,把你的本地提交“顶”到远程提交的最前面,保持提交历史是一条直线,没有分叉。
  • git push把本地代码推送到远程

    • git push origin [分支名]:推送到指定分支。
    • git push -u origin [分支名]第一次推送时用。关联本地和远程分支,以后再推只需要敲 git push 即可。
    • git push --force(或 -f):危险! 强制推送。覆盖远程仓库的历史。除非你知道自己在干什么(比如只有你一个人在用这个分支),否则千万别在团队项目里用。

后悔与撤销

后悔与撤销

这也是 Git 强大的地方,但比较容易晕,请仔细看。

  • git checkout -- [文件名] / git restore [文件名]丢弃“工作区”的修改

    • 场景:你改乱了一个文件,还没 add,想把它恢复成改动前的样子。
  • git reset版本回退(时光穿梭)

    • git reset --soft HEAD^软重置。撤销最近一次 commit,但保留代码在暂存区。

      • 场景:提交完觉得不满意,想撤回来重新调整一下再提交。
    • git reset --hard HEAD^硬重置。撤销最近一次 commit,删除所有改动,代码彻底回到上个版本。

      • 场景:最近写的全是垃圾,我完全不要了。
    • 注:HEAD^ 表示上一个版本,HEAD~2 表示往上两个版本。

  • git log查看历史

    • git log --oneline常用。一行显示一个提交,简洁。
    • git log --graph:以图形化方式显示分支合并历史。

场景速查

场景命令组合
我要把代码交上去git add . -> git commit -m "..." -> git push
我切错分支了,想切回来git switch [目标分支]
我看不到同事刚推的代码git pull
我刚才的提交注释写错了git commit --amend (然后修改保存)
这文件我改烂了,想还原git restore [文件名]
想看所有人的提交轨迹git log --oneline --graph --all

语法详解

分支合并

git merge

git merge 是 Git 中最核心的功能之一,它的作用是把两个分叉的“平行宇宙”(分支)合并成一个

对于新手来说,Merge 最让人困惑的地方在于:有时候它默默就合并完了,有时候却会生成一个新节点,而有时候又会直接报错(冲突)。

我们要分三种情况来理解它。

核心原则:站在哪里合并谁?

这是一个“主语”和“宾语”的问题,千万别搞反。

原则: 你必须先切换到接收修改的那个分支(通常是主干),然后把别的分支拉过来。

bash
# 场景:我想把 dev 分支的代码合并到 main 分支
git switch main    # 1. 先站到 main 上 (接收方)
git merge dev      # 2. 把 dev 拉过来 (贡献方)
策略1:快进模式

策略一:快进模式 (Fast-Forward) —— "顺水推舟"

这是最简单的情况。

  • 前提: 自从你创建 dev 分支后,main 分支没有任何新的提交,一直在原地等你。
  • 动作: Git 只需要把 main 的指针直接“滑”到 dev 的位置即可。
  • 特点: 会生成新的 Commit 节点,历史记录是一条直线。

image-20260120112140567

语法:

bash
git merge dev
# (Git 默认会自动判断是否可以使用快进模式)
策略2:普通合并 --no-ff

策略二:普通合并 (Non-Fast-Forward) —— "打个结"

这是最推荐的团队协作方式。

  • 前提: main 分支在你开发期间也有了新的变动(分叉了);或者你强制 Git 必须留下合并记录。
  • 动作: Git 会自动创建一个新的“合并提交”(Merge Commit),把两个分支的历史联结在一起。
  • 特点: 历史记录会出现一个“圈”或“菱形”,能清晰地看出这里发生过一次合并。

image-20260120112553318

语法:

bash
git merge --no-ff dev
  • --no-ff (No Fast Forward)关键参数! 即使可以快进,也强制生成一个新的 Merge 节点。
  • 为什么要这么做? 以后查历史时,你能清楚地看到:“哦,这一段代码是当时为了做‘用户登录’功能而合并进来的”,而不是混在主干里分不清。
策略3:压缩合并 --squash

策略三:压缩合并 (Squash)

如果你在 dev 分支上写了 50 个琐碎的提交(比如“修复错别字”、“又修复错别字”),你不想把这堆垃圾扔进 main 的历史里。

  • 效果:dev 上的所有修改打包成一个新的变化,放在暂存区。
  • 注意: 此时还没 Commit!你需要手动执行 git commit -m "完成用户登录功能"
  • 结局: main 分支上只会多出一个干干净净的提交,而不是 50 个。

image-20260120111426177

语法:

bash
git merge --squash dev
策略选择
你的需求推荐命令历史记录样子
个人开发,想快速同步git merge dev (默认 FF)一条直线,简洁
团队协作,需要保留功能开发记录git merge --no-ff dev有分叉和合并点 (像项链)
Feature 分支太乱,想净化主干git merge --squash dev主干只增加一个干净的点
解决冲突

解决冲突:这是新手的噩梦,但其实不可怕。

  • 原因: 你在 dev 分支改了第 10 行,同事在 main 分支也改了第 10 行。Git 懵了:“我不确定该听谁的,你自己决定吧。”
  • 现象: 执行 merge 后,Git 报 CONFLICT,并且自动停止。

解决步骤:

  1. 打开文件:你会看到代码里出现了一堆乱码符号(<<<<<<<, =======, >>>>>>>)。

    • <<<<<<< HEAD:当前分支(main)的代码。

    • >>>>>>> feature:要把合并进来的(feature)代码。

    • =======:分割线。

    image-20260120113226512

  2. 手动修改:删掉这些符号,保留你想要的代码(可以两个都留,也可以只留一个)。

  3. 提交

    bash
    git add .            # 告诉 Git 我修好了
    git commit           # 完成合并 (不需要写 -m,Git 会自动生成 "Merge branch 'dev'" 的信息)

git rebase

git rebase变基,是 Git 中最优雅但也最“危险”的高级技巧。

如果说 git merge 是把两条河流汇聚成一个湖泊(保留了汇聚的过程),那么 git rebase 就是把你的支流直接“嫁接”到主河道的最前端,让历史看起来像是一条直线,从来没有分叉过。

用法1:更新代码 (保持直线)

用法1:更新代码 (保持直线)

这是最常用的场景:你想把主干 (main) 的最新代码同步到你的分支 (feature),但你不想产生那个丑陋的“Merge branch 'main' into feature”的提交记录。

语法

bash
# 1. 切回你的分支
git switch feature

# 2. 执行变基 (把 main 当作我的新地基)
git rebase main

发生了什么

  1. Git 会把你 feature 分支上独有的提交(C、D)先“存”起来。
  2. feature 的指针指向 main 的最新提交(B)。
  3. 把存起来的 C 和 D,依次重新应用(Replay)在 B 后面。
  4. 结果: 历史记录变成直线:A -> B -> C' -> D'

2026-1-20-git-rebase

用法2:交互式变基 (整理历史)

用法2:交互式变基 (整理历史)(推荐

这是 Rebase 的杀手级功能。 有些公司要求:“每个 Pull Request 只能有一个 Commit”。但你在开发时可能提交了 10 次(比如“保存”、“修个bug”、“又修个bug”)。 你可以用交互式 Rebase 把这 10 个垃圾提交压缩(Squash) 成一个完美的提交。

语法

bash
# 整理最近的 4 次提交
git rebase -i HEAD~4
  • -i 意思是 Interactive (交互式)。

操作界面

执行命令后,Git 会弹出一个编辑器(如 Vim),显示类似下面的内容:

你需要修改每一行前面的指令(默认为 pick):

指令缩写含义作用
pickp保留原封不动保留这个提交。
rewordr修改保留代码,但允许你修改 Commit Message(提交注释)。
squashs挤压核心功能。把这个提交合并到上一个提交里去。
dropd丢弃彻底删掉这个提交(代码也会没)。
fixupf修正类似 squash,但直接丢弃注释,只留代码。

例子: 如果你想把第 2/4 个提交合并到第 1/3 个里,就把后 2/4 行的 pick 改成 s (squash),保存退出即可。

image-20260120115433641

解决冲突

解决冲突

在 Rebase 的过程中(Replay),每一层提交都可能冲突。

  1. Git 暂停:报冲突,停下来等你修。

  2. 解决冲突:打开文件,手动修好。

  3. 添加到暂存区git add .

  4. 继续变基注意:这里不是 commit):

    bash
    # 继续变基
    git rebase --continue
    
    # 放弃这次变基,回到原点
    git rebase --abort
禁止的操作

永远不要在“公共分支”上使用 Rebase

  • 公共分支:指推送到 GitHub 上、同事们都在用的分支(如 main, dev)。
  • 私有分支:只有你自己用的分支。

为什么

Rebase 会修改历史(Commit ID 会变)。

如果你把 main 分支 rebase 了,而你的同事是基于旧的 main 开发的。当你强推上去后,你同事的代码历史就彻底乱了,Git 会迫使他们手动合并大量冲突,你会被同事打死的。

一句话总结: git rebase 只能用于你自己的、还没推送到远程(或者只有你一个人在用)的分支

Merge vs Rebase

Merge vs Rebase

假设你正在 feature 分支开发,而 main 分支有了新的提交。

  • Merge(合并)
    创建一个新的合并节点(Merge Commit)。两条线最终汇合,历史变成了网状。
  • Rebase(变基)
    把你分支上的改动“暂时拿下来”,把地基换成 main 的最新位置,然后再把你刚才的改动“重新播放”一遍。
维度MergeRebase
历史形态真实、网状、有点乱干净、直线、像故事书
安全性极其安全,不会破坏历史有风险,改写历史
操作难度简单 (一键合并)中等 (可能需多次解冲突)
适用场景公共分支合并 (dev -> main)个人分支同步主干 (main -> my_feature)

git cherry-pick

git cherry-pick 是 Git 里的精准手术刀只把别的分支上的某一个(或几个)特定的提交,复制到当前分支来。

相比于 git merge(大卡车进货,把整个分支拖过来)和 git rebase(整条生产线搬迁),git cherry-pick 的操作更精细。

应用场景

场景一:紧急修复 (Hotfix)

  • 你在 dev 分支上开发功能,修好了一个严重的 Bug(提交 ID: a1b2c3),但 dev 上还有很多没写完的烂尾代码,不能发布。
  • 需求: 只要那个 Bug 修复,不要其他的垃圾。
  • 操作: 切到 main,把 a1b2c3 挑过来。

场景二:提交错了分支

  • 你在 main 上写了个功能,突然发现:“哎呀,这应该写在 feature 分支上的!”
  • 操作: 切到 feature,把刚才那个提交 cherry-pick 过来,然后切回 main 把那个提交删掉。
基本语法

基本语法

前提: 请先切换到接收代码的那个分支(目标分支)。

bash
git cherry-pick [参数] [Commit ID]
  • -e / --edit:在提交之前打开编辑器,允许你修改提交信息 (Commit Message)。

    • 默认情况下,cherry-pick 会直接使用原提交的信息。使用此选项可以补充说明,例如“Backport from feature branch”。

      bash
      # 执行后,Git 会弹出编辑器让你重新写 Commit Message。
      git cherry-pick -e f8c5b2a
  • -n / --no-commit:只将变更应用到工作区和暂存区,但不自动产生新的提交

    • 场景:你想挑选多个提交的内容,然后手动合并成一个新的提交。
    bash
    # 执行后,代码过来了,但没生成 Commit。你可以手动修改一下,再 git commit。
    git cherry-pick -n f8c5b2a
  • -x:在提交信息的末尾自动追加一行 cherry picked from commit ...

    • 非常有用!这能让你在未来追踪这个提交到底是从哪里来的,保持历史的可追溯性。
  • -m / --mainline仅用于挑选合并提交 (Merge Commit)。指定哪一个父节点作为主线。

    • 因为合并提交有两个父节点,Git 不知道该相对于哪一边计算变更。通常 -m 1 代表保留主分支的视角。

示例: 假设 dev 分支有个提交 f8c5b2a 是修复登录 Bug 的。

bash
git switch main           # 1. 回到主干
git cherry-pick f8c5b2a   # 2. 把那个提交“摘”过来
  • 结果: Git 会自动把那个提交里的修改内容拿过来,在 main 分支上生成一个新的提交。

  • 注意: 虽然内容一样,但 Commit ID 是新的!(因为父节点变了)。

用法1:挑选多个非连续提交

用法1:挑选多个非连续提交

如果你想把 dev 上的 A、C、E 提交拿过来,跳过 B 和 D:

bash
git cherry-pick [ID_A] [ID_C] [ID_E]
# 比如:git cherry-pick a1b2c3 d4e5f6 g7h8i9
用法2:挑选连续区间

用法2:挑选连续区间

如果你想把从 A 到 Z 之间的所有提交都拿过来:

  • 不包含 A (开区间): (A, Z]

    bash
    git cherry-pick A..Z
  • 包含 A (闭区间): [A, Z]

    A^ 表示 A 的前一个版本,所以范围就涵盖了 A

    bash
    git cherry-pick A^..Z
解决冲突

解决冲突

和 Merge/Rebase 一样,Cherry-pick 也会冲突(比如你要摘的那个文件,在当前分支已经被改得面目全非了)。

流程如下

  1. Git 暂停报错: 提示 conflict

  2. 手动修代码: 打开文件,解决冲突。

  3. 添加到暂存区: git add .

  4. 继续摘取:

    bash
    # 注意:这里不是 commit
    git cherry-pick --continue
    • 如果要放弃: git cherry-pick --abort (回到操作前的状态)。

    • 如果要跳过这个提交: git cherry-pick --skip (这个提交太难修了,我不要了,继续下一个)。

注意事项

虽然 Cherry-pick 很爽,但尽量少用,或者说不要把它作为常规工作流

原因:它是复制提交

如果你在 dev 做了一次 cherry-pick 到 main,以后你再把 dev merge 回 main 时,Git 会发现有两个“内容一样但 ID 不一样”的提交。 虽然 Git 通常足够聪明能忽略重复内容,但如果代码发生过微调,很容易产生不必要的冲突,导致历史记录混乱

历史记录

git reset

git reset 是 Git 中最强大但也最容易让人晕头转向的命令之一。如果不小心用错参数,可能会导致代码丢失。

为了让你彻底理解,我们先建立一个核心概念:Git 的版本管理其实是三个区域的配合。git reset 的不同参数,决定了它回退时会影响到哪几个区域

核心概念:三个区域

在执行回退之前,请脑补这三个区域:

  1. 历史库 (HEAD): 已经 commit 过的历史记录。
  2. 暂存区 (Index/Stage): git add 之后待提交的内容。
  3. 工作区 (Working Tree): 你正在写代码的文件(还没 add 的)。
基本语法
bash
git reset [模式] [目标版本]
  • 目标版本:通常是 HEAD^(上一个版本)、HEAD~3(往上3个版本)或具体的 Commit ID(如 a1b2c3d)。
  • 模式:主要有三种 --soft--mixed(默认)、--hard
三种模式详解

我们将通过一个场景来解释:

场景: 你写了一堆代码,执行了 add,又执行了 commit。现在你后悔了,想撤销这次 commit

--soft

模式一:--soft (软重置) —— 最温和

  • 口语解释: “我只是撤销了‘提交’在这个动作,代码别动,暂存状态也别动。”

  • 命令: git reset --soft HEAD^

  • 影响:

    • 历史库 (HEAD): 回退到上一个版本 ✅
    • 暂存区 (Index): 保留(刚才提交的代码还在暂存区,处于 git add 后的状态)。
    • 工作区 (Work): 保留(代码文件内容完全不变)。
  • 适用场景:

    • 提交完发现注释写烂了,想重新写。
    • 把多次琐碎的 commit 合并成一个大的 commit(先 soft reset 回去,再重新 commit 一次)。
--mixed

模式二:--mixed (混合重置 / 默认模式) —— 中等

  • 口语解释: “撤销‘提交’和‘放入暂存区’这两个动作,把代码退回到还没 add 的状态。”

  • 命令: git reset HEAD^ (不加参数默认就是 mixed)

  • 影响:

    • 历史库 (HEAD): 回退到上一个版本 ✅
    • 暂存区 (Index): 清空(刚才提交的代码被“拉”了出来,变成了未 add 状态)。
    • 工作区 (Work): 保留(代码文件内容完全不变)。
  • 适用场景:

    • 你 commit 了错误的文件,想撤销后重新挑选文件进行 add
    • 这是最常用的回退方式,因为代码都在,只是状态变了。
--hard

模式三:--hard (硬重置) —— 危险且强力

  • 口语解释: “我要穿越时空,把一切都恢复成那个版本的样子。刚写的代码我全都不要了!”

  • 命令: git reset --hard HEAD^

  • 影响:

    • 历史库 (HEAD): 回退到上一个版本 ✅
    • 暂存区 (Index): 重置(回到上个版本的状态)。
    • 工作区 (Work): 重置(你刚才写的所有新代码全部消失,文件内容变回上个版本)。
  • 适用场景:

    • 当前代码写得太烂了,完全无法挽回,想彻底放弃,重头再来。
    • 警示: 使用此命令前,请确保你真的不需要现在的代码了,因为找回很难。
图解对比表
模式历史库 (HEAD)暂存区 (Stage)工作区 (Work)你的代码去哪了?
--soft❌回退✅保留 (已 add)✅保留在暂存区,随时可以再次 commit
--mixed❌回退❌回退 (未 add)✅保留变成了红色的“修改未提交”状态
--hard❌回退❌回退❌回退彻底消失 (除非用 reflog 找回)
应用:撤销暂存的文件

撤销暂存的文件

git reset 不仅仅用于回退版本,常用于把文件从暂存区拿出来(Unstage)。

场景: 你不小心执行了 git add . 把所有文件都放进去了,但其中有一个 secret.txt 你不想提交。

  • 命令:
bash
git reset HEAD secret.txt
  • 注意:这里没有 --hard--soft,且后面跟的是文件名

  • 效果:

    • secret.txt 会从暂存区移出来。
    • 文件内容本身不会变,它只是变回了“未 add”的状态。
应用:误用--hard恢复

误用--hard恢复

如果你手滑执行了 git reset --hard,发现重要的代码没了,别慌,还有一招终极解法:Git Reflog

Git 会记录你的每一次 HEAD 移动(即使是被 reset 掉的)。

  1. 输入命令: git reflog

    • 你会看到类似这样的列表:
    • e4b2c1d HEAD@{0}: reset: moving to HEAD^ (这是你刚才闯的祸)
    • f8a9b0c HEAD@{1}: commit: 完成了登录功能 (这是你想要找回的那个提交)
  2. 找到救命 ID: 找到你 reset 之前的那个 Commit ID(比如上面的 f8a9b0c)。

  3. 穿越回去:

    bash
    git reset --hard f8a9b0c

你的代码就复活了!

git revert

git revert 是处理公开代码错误的最佳工具。

如果在 git reset 那个章节,你的关键词是“时光倒流”(直接删掉过去),那么 git revert 的关键词就是“负负得正”。

它不会删掉任何历史记录,而是生成一个新的提交,这个新提交的内容正好与你想撤销的那个提交完全相反

git revert 的必要性

想象一下,你和同事正在一起盖一座高楼(一条时间线):

  • Reset (回退): 你发现第 50 层盖歪了,于是你强行把第 50 层炸掉。

    • 后果: 你的同事如果已经在第 50 层上面盖了第 51 层,你这一炸,他的第 51 层就悬空了,大家的代码全乱套了。
  • Revert (反做): 你发现第 50 层盖歪了,你不拆掉它,而是在第 51 层盖了一个“反向修正层”来抵消第 50 层的错误。

    • 后果: 楼层继续往上盖,历史记录完整,同事的代码也不会受影响。
基本语法
bash
git revert [参数] [Commit ID]
  • Commit ID: 你想要撤销的那一次提交的哈希值(比如 a1b2c3d)。
  • 参数:主要有两种参数 --no-edit--no-commit
  • 执行后,Git 会自动弹出一个编辑器,让你写一个新的提交信息(默认是 Revert "原提交信息")。保存退出后,一个新的 Commit 就生成了。

示例

  1. 假设当前历史是:A -> B -> C (坏)

  2. 你执行 git revert C注意是C,表示要撤销C,不是B)。

  3. 现在的历史变成了:A -> B -> C -> C反 (C的相反操作)

    代码的状态实际上回到了 B,但历史记录里 C 依然存在。

常用参数

虽然基本命令够用,但以下参数能让你处理复杂情况更顺手:

--no-edit

--no-edit: 直接用默认的提交信息,别弹出编辑器让我确认了。

  • 命令: git revert [Commit ID] --no-edit
  • 场景: 你非常确定要撤销,懒得还要进 Vim/Nano 编辑器敲个回车。
--no-commit

--no-commit缩写:-n批量处理神器 ,把撤销的改动放到暂存区和工作区,但先不要自动生成新的 Commit

  • 命令: git revert -n [Commit ID]

  • 场景:

    • 你刚才连续提交了 3 个有 Bug 的版本(Commit A, B, C)。

    • 如果你直接 revert 三次,历史记录会多出 3 条 revert 记录,太丑了。

    • 你可以:

      bash
      git revert -n A
      git revert -n B
      git revert -n C
      git commit -m "一次性撤销 A, B, C 的所有错误"
    • 这样可以用一个新的提交,一次性抵消掉前面多个错误。

进阶:撤销合并节点

进阶:撤销合并节点(Revert Merge)

这是一个深坑,新手慎入。

如果你想撤销一次 Merge(比如把 feature 分支合到了 main,结果发现合错了),直接 git revert [Merge ID] 是会报错的。

因为 Merge 节点有两个“父亲”(Parent),Git 不知道你要回退到哪一边。

  • 语法: git revert -m [数字] [Merge ID]
  • 参数: -m 1 通常代表保留当前所在分支的内容(主干),撤销合并进来的那条分支。
解决冲突

解决冲突

就像 merge 会冲突一样,revert 也可能冲突。比如你想撤销很久以前的一行代码,但这行代码后来又被别人改过了。

Git 会停下来让你手动修文件。流程如下:

  1. 执行 revert,Git 报错“Conflict”。

  2. 手动打开文件,解决冲突(保留你想要的)。

  3. git add [文件名]

  4. 注意: 这里不要用 git commit,而是用:

    bash
    git revert --continue
Reset vs Revert

Reset vs Revert

这是面试和实战中最关键的区别,请务必记牢:

维度Git ResetGit Revert
操作方向向后退 (时光倒流)向前走 (亡羊补牢)
历史记录历史被删除/修改 (变干净)历史新增一条记录 (变长)
安全性危险 (可能丢代码)安全 (永远可以再 revert 回来)
适用场景本地 (还没 push 出去) 的私有代码远程 (已经 push 出去) 的公共代码

一句话口诀: 只要代码已经推送到 GitHub 给了别人,永远只用 Revert,绝对别用 Reset!

git stash

git stash 是 Git 中最实用的“救火”指令。

Commit vs Stash:如果把 Git 仓库比作你的书桌

  • commit:是把文件归档进抽屉(永久保存)。
  • stash:是把桌面上乱七八糟的文件先扫进临时储物箱,让书桌瞬间变干净,方便你干别的事。等你忙完了,再把储物箱里的东西倒回桌面上继续做。

经典使用场景

你正在 dev 分支狂写代码,写了一半,功能还没跑通(不能 commit)。突然老板冲过来说:“线上有个严重 Bug,马上切到 master 分支去修!”

这时候你面临两个问题:

  1. 代码没写完,不想提交脏代码
  2. 如果不提交,Git 禁止你切换分支(因为会覆盖修改)。

git stash 就是为此而生的。

核心命令
存:git stash

作用: 把所有已跟踪(Tracked)文件的修改(包括暂存区和工作区)保存起来,并把工作区还原成上一次 Commit 的干净状态。

语法

bash
git stash
  • 执行完后,你的代码就“消失”了(其实在储物箱里),你现在可以自由切换分支了。
取:git stash pop/apply

等你修完 Bug 回来,想继续写刚才的代码:

  • 方式 A(推荐):恢复并删除存档

    bash
    git stash pop
    • 作用: 把最近一次 stash 的内容应用回工作区,如果成功,就自动把这个 stash 删掉

    • 类比:剪贴板的“剪切粘贴”。

  • 方式 B(保守):恢复但保留存档

    bash
    git stash apply
    • 作用: 把 stash 的内容应用回工作区,但不删除 stash 列表里的记录。

    • 类比:剪贴板的“复制粘贴”。

常用参数

默认的 git stash 有两个大坑:

  1. 不存新建的文件(Untracked files)。
  2. 它存的名字是自动生成的(比如 WIP on master...),存多了根本不知道哪个是哪个。

请务必掌握以下两个参数:

--include-untracked

--include-untracked缩写:-u存新文件

bash
git stash -u
  • 默认情况下,如果你新建了一个文件但还没 git add 过,git stash 会无视它。切分支时这个文件可能会产生冲突或丢失。加上 -u,Git 才会把这些“新文件”也一起收进储物箱。
-m "说明信息"

-m "说明信息"给存档起名

bash
git stash -m "登录功能写了一半,暂停去修Bug"
  • 给你的 stash 加个注释。当你 stash 了好几次后,你会感谢这个习惯的。
管理多个 Stash

管理多个 Stash(堆栈管理)

Git 的 stash 是一个(Stack)结构,即使你 stash 了很多次,它们都在里面。

  • 查看列表

    bash
    git stash list

    输出示例:

    text
    stash@{0}: On master: 登录功能写了一半...
    stash@{1}: On dev: 昨天的临时修改...
    • stash@{0} 永远是最新存进去的那一个。
  • 应用指定的 stash:如果你想恢复 stash@{1} 而不是最新的 stash@{0}

    bash
    git stash pop stash@{1}
  • 删除指定的 stash

    bash
    git stash drop stash@{1}
  • 清空所有 stash (慎用)

    bash
    git stash clear
工作流程
  1. 突发状况: 正在写代码 -> 需要切分支。
  2. 保存: git stash -u -m "写了一半" -> 工作区变干净。
  3. 干活: 切分支 -> 修 Bug -> 提交。
  4. 回来: 切回原来的分支。
  5. 恢复: git stash pop -> 继续刚才的工作。
进阶:从 Stash 创建分支

如果你 stash 之后很久才回来,发现原来的分支已经改得面目全非了,这时候直接 pop 可能会有大量冲突。

你可以用这个命令,直接把 stash 的内容变成一个新的分支

bash
git stash branch [新分支名] [stash]

这会创建一个新分支,把你 stash 时的状态完整复原进去,非常安全

git stash branch 工作原理

常见实战

常见问题

TUN 模式下 git push 遭遇网络问题

git push 时报错

  • 报错

    sh
    > git push github main:main
    kex_exchange_identification: Connection closed by remote host
    Connection closed by 20.27.177.113 port 22
    fatal: Could not read from remote repository.
    
    Please make sure you have the correct access rights
    and the repository exists.
  • 分析

    这个错误 kex_exchange_identification: Connection closed by remote host 的核心意思是:SSH 握手阶段就被强行切断了

    这几乎可以肯定是因为 GitHub 的默认 SSH 端口(22 端口)在你当前的网络环境下被防火墙干扰或阻断了

解决方案

  1. 方案一:关闭 TUN 模式
  2. 方案二:使用 ssh.github.com 替代 github.com

方案一:关闭 TUN 模式

关闭 TUN 模式:检查出梯子使用了 TUN 模式,关闭 TUN 模式后再次 push 就成功了。

方案二:使用 ssh.github.com 替代 github.com

以下是关于 “如何在开启 v2rayN TUN 模式下,稳定进行 Git SSH 推送” 的终极解决方案总结。

核心原理:

在 TUN 模式下,代理软件会接管网卡流量。此时,传统的“本地端口监听”(如 127.0.0.1:10808)可能会因为防回环机制或端口独占而被关闭或屏蔽,导致使用 ProxyCommand 强行指定端口时报错 errno=10061

最佳策略是: 放弃指定本地代理端口,改用 ssh.github.com (443 端口)。这会让 SSH 流量伪装成 HTTPS 流量,由 TUN 网卡自然捕获并代理,既稳定又无需关心本地端口号。

操作步骤

步骤1:修改 SSH 配置文件:

这是最关键的一步。

  • 文件位置: C:\Users\你的用户名\.ssh\config

  • 修改内容: 将你的 GitHub 配置修改为以下格式(尤其是 HostnamePort,并注释掉 ProxyCommand):

    text
    # === 专为 TUN 模式优化的配置(代理软件关闭时也可用) ===
    Host github-proxy
     User git
     # 使用 ssh.github.com 替代 github.com
     Hostname ssh.github.com
     # 强制使用 443 端口 (防火墙友好,且利用 TUN 自动代理)
     Port 443
     # 重点:务必注释掉 ProxyCommand!
     # ProxyCommand connect -H 127.0.0.1:10808 %h %p
    
    # === (可选) 如果你想保留原版直连配置 ===
    Host github.com
     User git
     Hostname github.com

步骤2:配置 Git 仓库远程地址:

确保你的 Git 仓库使用的是你在 config 文件中定义的别名(这里是 github-proxy)。

  • 在项目根目录下打开终端:

  • 修改远程地址命令:

    bash
    git remote set-url origin git@github-proxy:用户名/仓库名.git

    (注意:将 用户名仓库名 替换为实际内容)

  • 或者也可以直接修改 .git/config 配置文件(效果同上)

    text
    [core]
    	repositoryformatversion = 0
    	filemode = false
    	bare = false
    	logallrefupdates = true
    	symlinks = false
    	ignorecase = true
    [remote "origin"]
    	url = git@github.com:用户名/仓库名.git
    	fetch = +refs/heads/*:refs/remotes/origin/*
    [remote "github-proxy"]
    	url = git@github-proxy:用户名/仓库名.git
    	fetch = +refs/heads/*:refs/remotes/origin/*
    [branch "main"]
    	remote = github-proxy
    	merge = refs/heads/main

步骤3:首次连接验证:

由于 ssh.github.com 的指纹与普通 github.com 不同,第一次连接时需要手动确认。

  • 执行命令:

    bash
    ssh -T git@github-proxy
  • 看到提示:

    Are you sure you want to continue connecting (yes/no/[fingerprint])?

  • 操作:

    输入 yes 并回车。

  • 成功标志:

    看到 Hi <你的用户名>! You've successfully authenticated...

方案优势:

  1. 无视端口变化: 不需要关心 v2rayN 的本地端口是 10808 还是 10809,也不怕端口被改。

  2. 兼容性强: 完美适配 TUN 模式,同时也兼容普通代理模式(只要代理软件处于运行状态,甚至代理软件关闭也可以使用)。

  3. 穿透性好: 使用 443 端口,在公司或学校等严苛网络环境下也能正常连接。

接下来,你可以愉快地使用 git push 了!